#!/bin/env python
"""Build DAOS Control Plane"""
#pylint: disable=too-many-locals
import sys
import os
import daos_build
from os.path import join, isdir
from os import urandom
from binascii import b2a_hex

Import('env', 'prereqs', 'daos_version', 'conf_dir', 'gurt_lib')

GO_COMPILER = 'go'
MIN_GO_VERSION = '1.12.0'
GO_BIN = env.get("GO_BIN", env.WhereIs(GO_COMPILER))

def check_dir_exists(path):
    """
    Check if path points to an existing directory. If it is a file but not a
    directory, delete it.
    """
    if not isdir(path):
        # Not a directory - clear out any file
        if os.path.exists(path):
            os.unlink(path)
        return False
    return True

def get_install_src_dir(repopath, name):
    """Get the Gopath-based directory to run 'go install' on"""
    return join(repopath, "src", "control", "cmd", name)

def get_install_bin_path(benv, name):
    """Get the final installation location for a binary with a given name"""
    return join(benv.subst("$PREFIX"), "bin", name)

def is_firmware_mgmt_build(benv):
    "Check whether this build has firmware management enabled."
    return benv["FIRMWARE_MGMT"] == 1

def get_build_tags(benv):
    "Get custom go build tags."
    if not is_firmware_mgmt_build(benv):
        return ""
    return "-tags firmware"

def is_release_build(benv):
    "Check whether this build is for release."
    return benv.get("BUILD_TYPE") == "release"

def get_build_flags(benv):
    """Return string of build flags"""
    flags = ""
    if not is_release_build(benv):
        # enable race detector for non-release builds
        flags += "-race"
    return flags

def install_go_manpage(menv, name):
    """
    Dynamically generate a manpage for a tool.
    """
    gen_bin = join('$BUILD_DIR', 'src/control/bin', name)
    build_path = join('$BUILD_DIR', 'src/control', name+'.8')
    install_path = join(menv.subst("$PREFIX"), "share/man/man8", name+'.8')
    menv.Command(build_path, [gen_bin, gurt_lib],
                '%s manpage -o %s' % (gen_bin, build_path))
    menv.InstallAs(install_path, build_path)
    return install_path

#pylint: disable=too-many-arguments
def install_go_bin(denv, gosrc, libs, name, install_name):
    """
    Build a Go binary whose source is under directory 'name' and install it
    as 'install_name'.
    libs should be a list of scons-built libraries, or None if none are needed.
    """
    # Repository path shared by DAOS Go package import paths
    repopath = join("github.com", "daos-stack", "daos")

    mod_src = join(gosrc, "cmd", name, "main.go") # Module src
    install_src = get_install_src_dir(repopath, name)
    install_bin = get_install_bin_path(denv, install_name)
    build_bin = join('$BUILD_DIR', 'src/control/bin', name)
    src = [mod_src]

    if libs is None:
        libs = []
    libs.extend(gurt_lib)

    def gen_build_id():
        """generate a unique build id per binary for use by RPM
           https://fedoraproject.org/wiki/PackagingDrafts/Go#Build_ID"""
        buildid = b2a_hex(urandom(20))
        if isinstance(buildid, str):
            # Python 2 is a str already, using decode() here transforms it
            # into a 'unicode' type
            return '0x' + buildid
        # Python 3 is a bytes, decode() it to a str
        return '0x' + buildid.decode()

    def go_ldflags():
        "Create the ldflags option for the Go build."
        path = 'github.com/daos-stack/daos/src/control/build'
        return ' '.join(['-ldflags',
                         '"-X {}.DaosVersion={}'.format(path, daos_version),
                         '-X {}.ConfigDir={}'.format(path, conf_dir),
                         '-B %s"' % gen_build_id()])
    # Must be run from the top of the source dir in order to
    # pick up the vendored modules.
    # Propagate useful GO environment variables from the caller
    if 'GOCACHE' in os.environ:
        denv['ENV']['GOCACHE'] = os.environ['GOCACHE']
    denv.Command(build_bin, src + libs,
                 'cd %s; %s build -mod vendor -v %s %s %s -o %s %s' %
                 (gosrc, GO_BIN, go_ldflags(), get_build_flags(denv),
                  get_build_tags(denv), build_bin, install_src))
    # Use the intermediate build location in order to play nicely
    # with --install-sandbox.
    denv.Requires(build_bin, ['gurt_lib'])
    denv.InstallAs(install_bin, build_bin)
    return install_bin
#pylint: enable=too-many-arguments

def configure_go(denv):
    """Do Go compiler checks"""
    if GetOption('help') or GetOption('clean'):
        return
    def check_go_version(context):
        """Check GO Version"""
        context.Display('Checking for Go compiler in $PATH... ')
        if GO_BIN:
            context.Display(GO_BIN + '\n')
        else:
            context.Result(0)
            return 0

        context.Display('Checking %s version... ' % GO_BIN)
        out = os.popen('%s version' % GO_BIN).read()
        if len(out.split(' ')) < 3:
            context.Result('failed to get version from "%s"' % out)
            return 0

        # go version go1.2.3 linux/amd64
        go_version = out.split(' ')[2].replace('go', '')
        if len([x for x, y in
                zip(go_version.split('.'), MIN_GO_VERSION.split('.'))
                if int(x) < int(y)]) > 0:
            context.Result('%s is too old (min supported: %s) '
                           % (go_version, MIN_GO_VERSION))
            return 0
        context.Result('%s' % go_version)
        return 1

    conf = Configure(denv, custom_tests={'CheckGoVersion': check_go_version})
    # pylint: disable=no-member
    if not conf.CheckGoVersion():
        print('no usable Go compiler found (yum install golang?)')
        sys.exit(1)
    conf.Finish()
    # pylint: enable=no-member


def scons():
    """Execute build"""
    env.AppendUnique(LIBPATH=[Dir('.')])

    denv = env.Clone()

    if denv.get("COMPILER") == 'covc':
        denv.Replace(CC='gcc', CXX='g++')

    # if SPDK_PREFIX differs from PREFIX, copy dir so files can be accessed at
    # runtime
    prefix = denv.subst("$PREFIX")
    sprefix = denv.subst("$SPDK_PREFIX")
    if sprefix not in ["", prefix]:
        def install_dir(ienv, srcdir, _destdir):
            """walk a directory and install targets"""
            for root, _dirs, files in os.walk(srcdir):
                dest_root = os.path.relpath(root, sprefix)
                for fname in files:
                    full_name = os.path.join(root, fname)
                    dest = os.path.join(prefix, dest_root)
                    ienv.Install(dest, full_name)

        for _dir in [join("share", "spdk", "scripts"), join("include", "spdk")]:
            target = join(prefix, _dir)
            source = join(sprefix, _dir)
            install_dir(denv, source, target)

    configure_go(denv)

    # Version-controlled DAOS Go source directory src/control
    gosrc = Dir('.').srcnode().abspath

    prereqs.require(denv, 'ofi', 'hwloc')
    old_cgold = denv["ENV"].get("CGO_LDFLAGS", "")
    # Sets CGO_LDFLAGS for rpath options
    daos_build.add_rpaths(denv, "..", True, True)
    denv.AppendENVPath("CGO_CFLAGS", denv.subst("$_CPPINCFLAGS"), sep=" ")
    if prereqs.client_requested():
        agentbin = install_go_bin(denv, gosrc, None, "daos_agent", "daos_agent")
        dmgbin = install_go_bin(denv, gosrc, None, "dmg", "dmg")
        if prereqs.test_requested():
            drpcbin = install_go_bin(denv, gosrc, None, "drpc_test",
                                     "hello_drpc")
            AlwaysBuild(drpcbin)

        AlwaysBuild([agentbin, dmgbin])

        Import('daos_hdlrs_lib')
        dbenv = denv.Clone()
        dblibs = dbenv.subst("-L$BUILD_DIR/src/gurt "
                            "-L$BUILD_DIR/src/cart "
                            "-L$BUILD_DIR/src/client/dfs "
                            "-L$BUILD_DIR/src/utils $_RPATH")
        dbenv.AppendENVPath("CGO_LDFLAGS", dblibs, sep=" ")
        daosbin = install_go_bin(dbenv, gosrc, [daos_hdlrs_lib], "daos", "daos")
        AlwaysBuild([daosbin])

        menv = denv.Clone()
        dmg_man = install_go_manpage(menv, "dmg")
        daos_man = install_go_manpage(menv, "daos")
        AlwaysBuild([dmg_man, daos_man])

    if not prereqs.server_requested():
        return

    senv = denv.Clone()
    denv = senv
    prereqs.require(denv, 'spdk')

    denv.AppendENVPath("CGO_CFLAGS", denv.subst("$_CPPINCFLAGS"), sep=" ")

    SConscript('lib/spdk/SConscript', exports='denv')

    denv.AppendUnique(LINKFLAGS=["-Wl,--no-as-needed"])
    prereqs.require(senv, 'pmdk', 'spdk', 'ofi', 'hwloc')
    daos_build.add_rpaths(denv, "..", True, True)

    cgolibdirs = senv.subst("-L$BUILD_DIR/src/control/lib/spdk "
                            "-L$BUILD_DIR/src/gurt "
                            "-L$BUILD_DIR/src/cart "
                            "-L$SPDK_PREFIX/lib "
                            "-L$OFI_PREFIX/lib $_RPATH")
    # Explicitly link RTE & SPDK libs for CGO access
    ldopts = cgolibdirs +                                                     \
             " -lspdk_env_dpdk -lspdk_nvme -lspdk_vmd -lrte_mempool" +        \
             " -lrte_mempool_ring -lrte_bus_pci -lnvme_control -lnuma -ldl"
    senv.AppendENVPath("CGO_LDFLAGS", ldopts, sep=" ")

    senv.AppendENVPath("CGO_CFLAGS",
                       senv.subst("-I$SPDK_PREFIX/include "
                                  "-I$OFI_PREFIX/include"),
                       sep=" ")

    # Copy setup_spdk.sh script to be executed at daos_server runtime.
    senv.Install(join('$PREFIX', 'share/daos/control'),
                 join(gosrc, 'server/init/setup_spdk.sh'))

    serverbin = install_go_bin(senv, gosrc, [denv.nvmecontrol], "daos_server",
                               "daos_server")

    #Admin binary is expected to be relocated so origin based paths are useless
    aenv = senv.Clone()
    aenv["ENV"]["CGO_LDFLAGS"] = old_cgold
    aenv.Replace(RPATH=[])
    # Sets CGO_LDFLAGS for rpath
    daos_build.add_rpaths(aenv, None, True, True)
    aenv.AppendENVPath("CGO_LDFLAGS", ldopts, sep=" ")
    adminbin = install_go_bin(aenv, gosrc, [denv.nvmecontrol], "daos_admin",
                              "daos_admin")

    AlwaysBuild([serverbin, adminbin])

    if is_firmware_mgmt_build(denv):
        print("(EXPERIMENTAL) Building DAOS firmware tools")
        fwbin = install_go_bin(aenv, gosrc, [denv.nvmecontrol], "daos_firmware",
                               "daos_firmware")
        AlwaysBuild([fwbin])

if __name__ == "SCons.Script":
    scons()
